﻿using System;
using System.Collections.Generic;
using PoolKit.Extensions;
using PoolKit.Strategies.Creation;

namespace PoolKit
{
    /// <summary>
    /// Pool able of constructing resources on the fly, if the underlying pool is drained, simulating the always-nonempty pool experience.
    /// 
    /// Note: this class is thread-safe.
    /// </summary>
    /// <typeparam name="TResource"></typeparam>
    internal class PoolToleratingStarvation<TResource> : Pool<TResource>
        where TResource : class
    {
        // TODO: got to protect against factories always returning same bullshit (or bullshit in general) -- target of opportunity for next releases.

        private readonly Pool<TResource> _poolWrapped;
        private readonly ResourceCreationStrategy<TResource> _resourceCreationStrategy;
        private readonly HashSet<Pooled<TResource>> _resourcesProducedAndGivenAway = new HashSet<Pooled<TResource>>();
        private readonly object _stateChangeCriticalSection = new object();
        private readonly DisposableCookie _disposableCookie = new DisposableCookie(Resources.PoolDisposed);

        /// <summary>
        /// 
        /// </summary>
        /// <param name="poolToWrap"></param>
        /// <param name="resourceCreationStrategy"></param>
        /// <exception cref="ArgumentNullException"></exception>
        public PoolToleratingStarvation(Pool<TResource> poolToWrap, ResourceCreationStrategy<TResource> resourceCreationStrategy)
        {
            if (poolToWrap == null) throw new ArgumentNullException("poolToWrap");
            if (resourceCreationStrategy == null) throw new ArgumentNullException("resourceCreationStrategy");

            _poolWrapped = poolToWrap;

            _resourceCreationStrategy = resourceCreationStrategy.ToThreadSafe();
        }

        /// <summary>
        /// Asks the pool to lend you a resource
        /// </summary>
        /// <returns>
        /// A resource borrowed from pool
        /// </returns>
        /// <exception cref="FailedToConstructResourceException"></exception>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public override TResource Lend()
        {
            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                TResource resource;

                if (!TryLend(out resource))
                {
                    throw new InvalidOperationException(Resources.AlwaysReadyPoolFailedToLendResource);
                }

                if (resource == null)
                {
                    throw new InvalidOperationException(Resources.AlwaysReadyPoolFailedToLendResource);
                }

                return resource;
            }
        }

        /// <summary>
        /// Asks the pool to give up a resource to you - after this the pool no longer tracks the
        /// resource and you cannot return it to the pool
        /// </summary>
        /// <returns>
        /// A resource from pool
        /// </returns>
        /// <exception cref="CannotGiveUpResourceException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public override TResource GiveUp()
        {
            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                return _poolWrapped.GiveUp();
            }
        }

        /// <summary>
        /// Asks the pool to lend you a resource
        /// </summary>
        /// <param name="resource">A resource borrowed from pool</param>
        /// <returns>True, if the resource has been lent, False - otherwise</returns>
        /// <exception cref="ObjectDisposedException"></exception>
        /// <exception cref="FailedToConstructResourceException"></exception>
        internal override bool TryLend(out TResource resource)
        {
            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                if (!_poolWrapped.TryLend(out resource))
                {
                    // Note: the creepy scenario of ensuring it's not an object it has given us before - is this even possible without looking at the state of underlying pool? - is
                    // left as a target of opportunity for future releases.
                    try
                    {
                        TResource newResource = _resourceCreationStrategy.Create();

                        if (newResource == null)
                        {
                            throw new FailedToConstructResourceException(Resources.FailedToCreateResourceDueToStrategyReturningNull.FormatWith(_resourceCreationStrategy.ToString()));
                        }

                        var newResourcePooled = new Pooled<TResource>(newResource);

                        lock (_stateChangeCriticalSection)
                        {
                            if (!_resourcesProducedAndGivenAway.Add(newResourcePooled))
                            {
                                throw new FailedToConstructResourceException(Resources.FailedToCreateResourceDueToStrategyCreatingDuplicates.FormatWith(_resourceCreationStrategy.ToString()));
                            }
                        }

                        resource = newResource;
                    }
                    catch (FailedToConstructResourceException)
                    {
                        throw;
                    }
                    catch (Exception exception)
                    {
                        if (exception.IsNotWrappable())
                        {
                            throw;
                        }

                        throw new FailedToConstructResourceException(Resources.FailedToCreateResourceDueToStrategyThrowing.FormatWith(_resourceCreationStrategy.ToString()), exception);
                    }
                }

                return true; 
            }
        }

        /// <summary>
        /// Returns a previously borrowed resource to the pool
        /// </summary>
        /// <param name="resource">A previously borrowed resource to be returned</param>
        /// <exception cref="CannotAcceptResourceException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public override void AcceptBack(TResource resource)
        {
            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                if (resource == null) throw new ArgumentNullException("resource");

                var pooled = new Pooled<TResource>(resource);

                if (_resourcesProducedAndGivenAway.Remove(pooled))
                {
                    return;
                }

                _poolWrapped.AcceptBack(resource);
            }
        }

        /// <summary>
        /// Returns a previously borrowed, broken resource to the pool
        /// </summary>
        /// <param name="resource">Broken resource to be returned.</param>
        /// <exception cref="ObjectDisposedException"></exception>
        /// <exception cref="CannotAcceptResourceException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public override void AcceptBackBroken(TResource resource)
        {
            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                if (resource == null) throw new ArgumentNullException("resource");

                var pooled = new Pooled<TResource>(resource);

                if (_resourcesProducedAndGivenAway.Remove(pooled))
                {
                    return;
                }

                _poolWrapped.AcceptBackBroken(resource);
            }
        }

        /// <summary>
        /// Disposes of the pool
        /// </summary>
        public override void Dispose()
        {
            lock (_stateChangeCriticalSection)
            {
                if (_disposableCookie.IsDisposed)
                {
                    return;
                }

                _disposableCookie.MarkAsDisposed();

                _poolWrapped.Dispose();
            }
        }
    }
}